Trabajo Preparacion, visualizacion de Datos y Machine learning con Python

Ciencia de datos en Produccion

Estudiante: Sebastián Cardona y Jose Miguel Millán

ID: 1094910122 y 1088334182

Email: sacardonar@uqvirtual.edu.co y josem.millanl@uqvirtual.edu.co

Docente: [Jose R. Zapata](https://joserzapata.github.io)

  • https://joserzapata.github.io
  • https://twitter.com/joserzapata
  • https://www.linkedin.com/in/jose-ricardo-zapata-gonzalez/

Objetivo del Trabajo

En el anterior experimento se entrenaron dos modelos de regresión considerando unos casos atípicos dentro del análisis, los modelos resultantes tuvieron un bajo desempeño, por lo tanto, en este experimento se eliminarán los datos atípicos encontrados.

Definir el Problema a Resolver¶

El dataset "house data" contiene información de casas vendidas en Kansas y características descriptivas de las mismas, inicialmente se realizará una exploración de datos para poder saber la calidad del dataset iniciando con una limpieza la cual consta de eliminar duplicados, identificación de datos atípicos, nulos o mal escritos para poder tratarlos y mitigarlos ya sea con la eliminación o aplicación de métodos estadísticos con la finalidad de tener un datset listo y poder aplicar una regresión lineal para predecir los precios de venta de una casa.

Describir los datos de entrada y salida¶

  • Cantidad de Variables
  • Tipo de Variables
  • Significado de cada Variable

Importar Librerias¶

In [1]:
%load_ext autoreload
%autoreload 2


from datetime import datetime
from pathlib import Path

import numpy as np
import pandas as pd
import plotly.express as px
import seaborn as sns
from IPython.display import display, HTML
from pandas_profiling.profile_report import ProfileReport
from sklearn.linear_model import SGDRegressor, Lasso, Ridge, LinearRegression, ElasticNet
from sklearn.metrics import r2_score
from sklearn.model_selection import cross_val_score, train_test_split, cross_validate, RepeatedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.svm import SVR
import phik
from tqdm import tqdm

from src.data.preprocessing import preprocessing
from src.features.build_features import build_features
from src.jutils.data import DataUtils
from src.jutils.visual import Plot
from src.data.procesamiento_datos import Preprocesamiento, LimpiezaCalidad, ProcesamientoDatos
import joblib
In [2]:
# Funciones utilizadas para realizar un preprocesamiento general.

def validar_duplicados(_df):
    _filas = _df.shape[0]
    _cant_duplicados = _df.duplicated().sum()
    print(f'De {_filas} registros hay {_cant_duplicados} filas duplicadas, representando el {_cant_duplicados/_filas:.2%}')

def eliminar_duplicados(_df):
    # Eliminando duplicados
    _df = _df.drop_duplicates(keep='first')
    _filas = _df.shape[0]
    print(f'Después de la eliminación de duplicados, el conjunto de datos queda con {_filas} filas.')
    return _df

def validar_index_duplicados(_df):
    # Validando duplicados de index
    _son_duplicados = _df['index'].duplicated()
    _cant_duplicados = _son_duplicados.sum()
    _filas = _df.shape[0]
    print(f'De {_filas} registros, hay {_cant_duplicados} registros con index duplicado, que representan el {_cant_duplicados/_filas:.2%}.')
    return _son_duplicados

def convertir_col_date_a_date(_df):
    _df['date'] = pd.to_datetime(_df['date'], errors='coerce')
    return _df

def reemplazar_valores_extremos(_df, _columnas_numericas):
    _df[_columnas_numericas] = _df[_columnas_numericas].where(lambda x: x > -1e+10, other=np.nan).where(
        lambda x: x < 1e+10, other=np.nan)
    return _df

def reemplazar_nulos_por_la_media(_df, _columnas_numericas):
    # Se reemplazan los valores nulos por la media Nota: No se considera que haya data leakage pues los valores
    # reemplazados son entre registros con el mismo index y como al final se va a dejar un dataset con index únicos,
    # no hay riesgo que estén tanto en el set de entrenamiento como en el de test
    for columna_numerica in _columnas_numericas:
        _df[columna_numerica] = _df[columna_numerica].fillna(
            _df.groupby('index')[columna_numerica].transform('median'))
    return _df

def reemplazar_fechas_nulas(_df):
    # Reemplazando fechas nulas por la primera fecha no nula
    _df['date'] = _df['date'].fillna(
        _df.groupby(['index'], sort=False)['date'].apply(lambda x: x.ffill().bfill()))
    return _df

def reemplazar_ceros_por_nulos(_df):
    # Reemplazando ceros por valores nulos
    _df[['sqft_basement', 'yr_renovated']] = _df[['sqft_basement', 'yr_renovated']].replace(0, np.nan)
    return _df
In [3]:
# Funciones utilizadas para procesar únicamente los datos de entrenamiento
def z_score_outliers(_df, _column):
    """
    Returns:
        zscore, outlier
    """
    # Adaptado de https://www.kaggle.com/code/shweta2407/regression-on-housing-data-accuracy-87
    #creating lists to store zscore and outliers 
    zscore = []
    isoutlier =[]
    # for zscore generally taken thresholds are 2.5, 3 or 3.5 hence i took 3
    threshold = 3
    # calculating the mean of the passed column
    mean = np.mean(_df[_column])
    # calculating the standard deviation of the passed column
    std = np.std(_df[_column])
    for i in _df[_column]:
        z = (i-mean)/std
        zscore.append(z)
        #if the zscore is greater than threshold = 3 that means it is an outlier
        isoutlier.append(np.abs(z) > threshold)
    return zscore, isoutlier
In [4]:
# Funciones para realizar el procesamiento de los datos antes de ingresar al modelo
def mediana_recortada_imputacion(_df, _column, _isoutlier):
    mediana_recortada = _df[_column][~_isoutlier].median()
    _df.loc[_isoutlier, _column] = mediana_recortada
    return _df

def calculo_variables_adicionales(_df):
    _df['tiene_sotano'] = (~_df['sqft_basement'].isna()).astype(int)
    _df['fue_renovada'] = (~_df['yr_renovated'].isna()).astype(int)
    _df['yr_date'] = _df['date'].dt.year
    _df['antiguedad_venta'] = _df['yr_date'] - _df['yr_built']
    return _df

def procesamiento_datos_faltantes(_df, _columnas):
    _df = _df.dropna(subset=_columnas)
    return _df

def clasificar_columnas(_df, _clasificacion_columnas):
    _df[_clasificacion_columnas['categorica_ordinal']] = _df[_clasificacion_columnas['categorica_ordinal']].astype(int)
    _df[_clasificacion_columnas['numerica_continua']] = _df[_clasificacion_columnas['numerica_continua']].astype(float)
    _df[_clasificacion_columnas['numerica_discreta']] = np.floor(_df[_clasificacion_columnas['numerica_discreta']])
    return _df
    
def imputacion_de_datos(_df, _columnas):
    _df[_columnas] = _df[_columnas].fillna(0)
    return _df
In [5]:
def profiler_to_file(_profiler, archivo):
    print('Ejecutando profiler')
    _profiler.to_file(du.data_folder_path.parent.joinpath('reports/' + archivo))
    return True

def calcular_descriptivas(_df):
    descriptivas = _df.describe()
    descriptivas.loc['rango'] = descriptivas.loc['max'] - descriptivas.loc['min']
    descriptivas.loc['IQR'] = descriptivas.loc['75%'] - descriptivas.loc['25%']
    descriptivas.loc['coef de var'] = descriptivas.loc['std']/descriptivas.loc['mean']
    descriptivas.loc['skewness'] = du.data.skew(numeric_only=True)
    descriptivas.loc['kurtosis'] = du.data.kurtosis(numeric_only=True)
    return descriptivas

plot = Plot()

Cargar Datasets¶

In [6]:
du = DataUtils(
    Path(r'..\data').resolve().absolute(),
    "kc_house_dataDS.parquet",
    'price',
    lambda path: pd.read_parquet(path),
    lambda df, path: df.to_parquet(path)
)
du.data = du.load_data(du.interim_path.joinpath(du.input_file_name))

Descripcion General del Dataset¶

  • numero de filas y columnas
  • tipos de datos y si estan correctos

Durante la exploración inicial se realizó la conversión de los tipos de datos y la correcta representación de datos nulos.

In [7]:
shape = du.data.shape
filas = shape[0]
columnas = shape[1]
print(f'El conjunto de datos se compone de {filas} filas y {columnas} columnas.')
El conjunto de datos se compone de 131994 filas y 23 columnas.
In [8]:
du.data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 131994 entries, 0 to 131993
Data columns (total 23 columns):
 #   Column         Non-Null Count   Dtype  
---  ------         --------------   -----  
 0   index          131994 non-null  int64  
 1   zipcode        118787 non-null  float64
 2   grade          118717 non-null  float64
 3   sqft_basement  118688 non-null  float64
 4   view           118854 non-null  float64
 5   bathrooms      118689 non-null  float64
 6   bedrooms       118729 non-null  float64
 7   sqft_above     118712 non-null  float64
 8   sqft_living15  118814 non-null  float64
 9   lat            118731 non-null  float64
 10  waterfront     118730 non-null  float64
 11  floors         118791 non-null  float64
 12  date           127544 non-null  object 
 13  yr_renovated   118728 non-null  float64
 14  yr_built       118712 non-null  float64
 15  long           118760 non-null  float64
 16  jhygtf         118728 non-null  float64
 17  sqft_lot       118763 non-null  float64
 18  price          118661 non-null  float64
 19  condition      118740 non-null  float64
 20  wertyj         131994 non-null  int64  
 21  sqft_lot15     118739 non-null  float64
 22  sqft_living    118768 non-null  float64
dtypes: float64(20), int64(2), object(1)
memory usage: 24.2+ MB

Todas las columnas son del tipo correcto a excepción de date, se deberá hacer la conversión de este campo.

Limpieza de calidad de datos general¶

  • Filas con valores exactamente iguales (duplicados)
  • Columnas duplicadas
  • Columnas con valores constantes o sin informacion
In [9]:
validar_duplicados(du.data)
De 131994 registros hay 1300 filas duplicadas, representando el 0.98%
In [10]:
du.data = eliminar_duplicados(du.data)
Después de la eliminación de duplicados, el conjunto de datos queda con 130694 filas.
In [11]:
# Validar indices duplicados
son_duplicados = validar_index_duplicados(du.data)
De 130694 registros, hay 108695 registros con index duplicado, que representan el 83.17%.
In [12]:
# Revisando los primeros registros duplicados
du.data[son_duplicados].sort_values(by='index').head()
Out[12]:
index zipcode grade sqft_basement view bathrooms bedrooms sqft_above sqft_living15 lat ... yr_renovated yr_built long jhygtf sqft_lot price condition wertyj sqft_lot15 sqft_living
44480 0 98178.0 7.0 0.0 0.0 1.0 NaN 1.180000e+03 1.340000e+03 47.5112 ... 0.0 1.955000e+03 NaN 0.0 5.650000e+03 221900.0 3.000000e+00 876543 5650.0 1180.0
59332 0 98178.0 7.0 NaN NaN 1.0 3.0 1.180000e+03 1.340000e+03 47.5112 ... 0.0 1.955000e+03 -122257.0 0.0 5.650000e+03 NaN 3.000000e+00 876543 5650.0 1180.0
89550 0 NaN NaN 0.0 0.0 1.0 3.0 1.180000e+03 NaN NaN ... 0.0 NaN -122257.0 0.0 5.650000e+03 221900.0 3.000000e+00 876543 5650.0 1180.0
27240 0 98178.0 7.0 0.0 0.0 1.0 3.0 1.180000e+03 1.340000e+03 47.5112 ... 0.0 1.955000e+03 -122257.0 0.0 5.650000e+03 221900.0 3.000000e+00 876543 5650.0 1180.0
50303 0 98178.0 7.0 0.0 0.0 1.0 3.0 -5.432346e+10 -5.432346e+10 47.5112 ... 0.0 -5.432346e+10 -122257.0 0.0 -5.432346e+10 221900.0 -5.432346e+10 876543 5650.0 1180.0

5 rows × 23 columns

Revisando los registros duplicados por index, se encuentra que muchas columnas tienen los mismos valores , lo único que cambia es que hay algunos faltantes y hay otros valores extremadamente bajos o altos, adicionalmente se observan algunos registros de la columna date que no son fechas. Primero se convertirá los valores de la columna date a date y los que no puedan ser convertidos se reemplazarán por valores nulos, luego se reemplazarán los valores extremos por valores nulos, luego se calculará la mediana por index para las columnas numéricas y se reemplazarán los valores nulos por estas medianas, luego se eliminarán filas duplicadas y se reevaluarán los index duplicados.

Si nuestra suposición es correcta, no importa realizar una imputación por la mediana pues todos los valores de los índices son iguales pues luego de hacer la imputación y eliminar nuevamente duplicados no deberían quedar índices duplicados, en caso de que sigan habiendo índices duplicados se deben revertir las imputaciones realizadas y buscar otra estrategia para eliminar duplicados exactos.

In [13]:
# Convirtiendo la columna date a datetime
du.data = convertir_col_date_a_date(du.data)
In [14]:
# Reemplazando valores extremos, menores a -1e+10 o mayores a 1e+10
columnas_numericas = [columna for columna in du.data.columns if columna != 'date']
du.data = reemplazar_valores_extremos(du.data, columnas_numericas)
In [15]:
# Se reemplazan los valores extremos por la media
# Nota: No se considera que haya data leakage pues los valores reemplazados son entre registros con el mismo index y como 
# al final se va a dejar un dataset con index únicos, no hay riesgo que estén tanto en el set de entrenamiento como en el de
# test
du.data = reemplazar_nulos_por_la_media(du.data, columnas_numericas)
In [16]:
# Reemplazando fechas nulas por la primera fecha no nula
du.data = reemplazar_fechas_nulas(du.data)

Para las siguientes columnas, un cero representa un dato nulo, por lo tanto se reemplazarán.

  • sqft_basement
  • yr_renovated
In [17]:
# Reemplazando ceros por valores nulos
du.data = reemplazar_ceros_por_nulos(du.data)
In [18]:
validar_duplicados(du.data)
De 130694 registros hay 108695 filas duplicadas, representando el 83.17%
In [19]:
du.data = eliminar_duplicados(du.data)
Después de la eliminación de duplicados, el conjunto de datos queda con 21999 filas.
In [20]:
son_duplicados = validar_index_duplicados(du.data)
De 21999 registros, hay 0 registros con index duplicado, que representan el 0.00%.

Una vez validados los índices duplicados, se evidencia que la limpieza surtió efecto (Se debe implementar un control que valide esto cuando se vaya a realizar un reentrenamiento, se debe alertar cuando sigan habiendo índices duplicados e interrumpir el proceso)

In [21]:
# Validando columnas con valores constantes
unicos=du.data.nunique()
unicos[unicos==1]
Out[21]:
wertyj    1
dtype: int64

La columna wertyj tiene valores constantes, por lo tanto se eliminará.

In [22]:
du.data = du.data.drop(columns=list(unicos[unicos==1].index))
In [23]:
nulos = du.data.isnull().sum()
cant_unicos = du.data.apply(lambda x: len(x.unique()))
porce = nulos/filas
nulos = pd.DataFrame({'nulos':nulos, 'porc':porce, 'cant_unicos': cant_unicos})
# Se contarán las filas que contengan algún dato nulo
al_menos_un_nulo=du.data.isnull().any(axis=1).sum()
nulos.sort_values(by='porc', ascending=False)
Out[23]:
nulos porc cant_unicos
yr_renovated 21069 0.159621 70
sqft_basement 13368 0.101277 306
price 30 0.000227 4027
zipcode 12 0.000091 71
sqft_above 12 0.000091 947
sqft_living 11 0.000083 1039
lat 9 0.000068 5035
jhygtf 9 0.000068 71
floors 9 0.000068 7
grade 9 0.000068 13
bathrooms 8 0.000061 31
yr_built 7 0.000053 117
waterfront 6 0.000045 3
sqft_lot 6 0.000045 9782
bedrooms 5 0.000038 14
condition 5 0.000038 6
view 4 0.000030 6
date 3 0.000023 373
long 3 0.000023 753
sqft_living15 1 0.000008 778
sqft_lot15 1 0.000008 8690
index 0 0.000000 21999
In [24]:
print(f'De {filas} registros, hay {al_menos_un_nulo} registros con al menos un valor nulo, representando el {al_menos_un_nulo/filas:.2%}')
De 131994 registros, hay 21531 registros con al menos un valor nulo, representando el 16.31%

Se debe tener en cuenta que para el caso de yr_renovated y sqft_basement, un valor nulo no representa necesariamente falta de información, para el caso de yr_renovated un nulo representa que esa casa nunca se renovó. Y en el caso de sqft_basement quiere decir que la casa no tiene sótano.

In [25]:
# Calculando variables adicionales
du.data = calculo_variables_adicionales(du.data)
In [26]:
# Esta vez no se tendrán en cuenta las columnas yr_renovated y sqft_basement
df = du.data.drop(columns=['yr_renovated', 'sqft_basement'])
nulos = df.isnull().sum()
cant_unicos = df.apply(lambda x: len(x.unique()))
porce = nulos/filas
nulos = pd.DataFrame({'nulos':nulos, 'porc':porce, 'cant_unicos': cant_unicos})
# Se contarán las filas que contengan algún dato nulo
al_menos_un_nulo=df.isnull().any(axis=1).sum()
nulos.sort_values(by='porc', ascending=False)
Out[26]:
nulos porc cant_unicos
price 30 0.000227 4027
sqft_above 12 0.000091 947
zipcode 12 0.000091 71
sqft_living 11 0.000083 1039
antiguedad_venta 10 0.000076 118
floors 9 0.000068 7
grade 9 0.000068 13
jhygtf 9 0.000068 71
lat 9 0.000068 5035
bathrooms 8 0.000061 31
yr_built 7 0.000053 117
waterfront 6 0.000045 3
sqft_lot 6 0.000045 9782
bedrooms 5 0.000038 14
condition 5 0.000038 6
view 4 0.000030 6
date 3 0.000023 373
long 3 0.000023 753
yr_date 3 0.000023 3
sqft_living15 1 0.000008 778
sqft_lot15 1 0.000008 8690
tiene_sotano 0 0.000000 2
fue_renovada 0 0.000000 2
index 0 0.000000 21999
In [27]:
print(f'De {filas} registros, hay {al_menos_un_nulo} registros con al menos un valor nulo, representando el {al_menos_un_nulo/filas:.2%}')
De 131994 registros, hay 109 registros con al menos un valor nulo, representando el 0.08%

Para el entrenamiento del primer modelo se eliminarán los datos nulos debido a su poca cantidad.

In [28]:
columnas_a_eliminar_nulos = du.data.drop(columns=['yr_renovated', 'sqft_basement']).columns
du.data = procesamiento_datos_faltantes(du.data, columnas_a_eliminar_nulos)
In [29]:
print(f'{du.data.shape=}')
du.data.shape=(21890, 26)

Dividir el dataset en Training set y Test set¶

In [30]:
train_test, validation = train_test_split(du.data, test_size=0.2, random_state=1)
du.save_data(train_test, du.raw_train_test_path)
du.save_data(validation, du.raw_validation_path)
print(f'{train_test.shape=}')
print(f'{validation.shape=}')
train_test.shape=(17512, 26)
validation.shape=(4378, 26)

Descripcion y Limpieza de los datos¶

In [31]:
du.data = du.load_data(du.raw_train_test_path)
print('Tipos de variables')
du.data.info()
Tipos de variables
<class 'pandas.core.frame.DataFrame'>
Int64Index: 17512 entries, 19857 to 237
Data columns (total 26 columns):
 #   Column            Non-Null Count  Dtype         
---  ------            --------------  -----         
 0   index             17512 non-null  int64         
 1   zipcode           17512 non-null  float64       
 2   grade             17512 non-null  float64       
 3   sqft_basement     6857 non-null   float64       
 4   view              17512 non-null  float64       
 5   bathrooms         17512 non-null  float64       
 6   bedrooms          17512 non-null  float64       
 7   sqft_above        17512 non-null  float64       
 8   sqft_living15     17512 non-null  float64       
 9   lat               17512 non-null  float64       
 10  waterfront        17512 non-null  float64       
 11  floors            17512 non-null  float64       
 12  date              17512 non-null  datetime64[ns]
 13  yr_renovated      732 non-null    float64       
 14  yr_built          17512 non-null  float64       
 15  long              17512 non-null  float64       
 16  jhygtf            17512 non-null  float64       
 17  sqft_lot          17512 non-null  float64       
 18  price             17512 non-null  float64       
 19  condition         17512 non-null  float64       
 20  sqft_lot15        17512 non-null  float64       
 21  sqft_living       17512 non-null  float64       
 22  tiene_sotano      17512 non-null  int32         
 23  fue_renovada      17512 non-null  int32         
 24  yr_date           17512 non-null  float64       
 25  antiguedad_venta  17512 non-null  float64       
dtypes: datetime64[ns](1), float64(22), int32(2), int64(1)
memory usage: 3.5 MB

Identificacion de Variables¶

  • Variables de entrada y de salida
  • Tipo de Variables (categoricas o Numericas)
  • Tipo de datos (int, float, string, factor, boolean, ...)
In [32]:
# Clasificación de columnas
clasificacion_columnas = {
    'categorica_ordinal': ['zipcode', 'grade', 'view', 'waterfront', 'condition', 'lat', 'long'],
    'fecha': ['date'],
    'id': ['index'],
    'numerica_continua': ['sqft_basement', 'sqft_above', 'sqft_living15', 'sqft_lot', 'price', 'sqft_lot15', 'sqft_living'],
    'numerica_discreta': ['bathrooms', 'bedrooms', 'yr_renovated', 'yr_built', 'jhygtf', 'yr_date', 'antiguedad_venta', 'floors']
}
columna_salida = 'price'
columnas_a_descartar = ['date', 'index']

du.data = du.data.drop(columns=columnas_a_descartar)

columnas_entrada = du.data.drop(columns=columna_salida).columns
print(f'Columna salida: {columna_salida}')
print(f'Columnas de entrada: {columnas_entrada}')
Columna salida: price
Columnas de entrada: Index(['zipcode', 'grade', 'sqft_basement', 'view', 'bathrooms', 'bedrooms',
       'sqft_above', 'sqft_living15', 'lat', 'waterfront', 'floors',
       'yr_renovated', 'yr_built', 'long', 'jhygtf', 'sqft_lot', 'condition',
       'sqft_lot15', 'sqft_living', 'tiene_sotano', 'fue_renovada', 'yr_date',
       'antiguedad_venta'],
      dtype='object')

Analisis General Univariable y Bivariable¶

Analisis de cada una de las variables para lograr calidad de datos en cada columna

  • Correccion del tipo de dato (numericas, categoricas, string) de cada columna (optimizar memoria)
  • Deteccion de numero de datos faltantes
  • Deteccion de duplicados
In [33]:
du.data= clasificar_columnas(du.data, clasificacion_columnas)
In [34]:
du.data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 17512 entries, 19857 to 237
Data columns (total 24 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   zipcode           17512 non-null  int32  
 1   grade             17512 non-null  int32  
 2   sqft_basement     6857 non-null   float64
 3   view              17512 non-null  int32  
 4   bathrooms         17512 non-null  float64
 5   bedrooms          17512 non-null  float64
 6   sqft_above        17512 non-null  float64
 7   sqft_living15     17512 non-null  float64
 8   lat               17512 non-null  int32  
 9   waterfront        17512 non-null  int32  
 10  floors            17512 non-null  float64
 11  yr_renovated      732 non-null    float64
 12  yr_built          17512 non-null  float64
 13  long              17512 non-null  int32  
 14  jhygtf            17512 non-null  float64
 15  sqft_lot          17512 non-null  float64
 16  price             17512 non-null  float64
 17  condition         17512 non-null  int32  
 18  sqft_lot15        17512 non-null  float64
 19  sqft_living       17512 non-null  float64
 20  tiene_sotano      17512 non-null  int32  
 21  fue_renovada      17512 non-null  int32  
 22  yr_date           17512 non-null  float64
 23  antiguedad_venta  17512 non-null  float64
dtypes: float64(15), int32(9)
memory usage: 2.7 MB

Eliminación de outliers¶

Primero se analizará la distribución de cada variable¶

In [35]:
box_plots = [plot.box(du.data, y=columna) for columna in clasificacion_columnas['numerica_continua']]
histograms = [plot.histogram(du.data, x=columna, text_auto=False) for columna in clasificacion_columnas['numerica_continua']]

plot.grid_subplot(*box_plots, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()
plot.grid_subplot(*histograms, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()
020004000 05k 200040006000 00.5M1M1.5M 02B4B 0200k400k600k800k 05k10k
Distribución inicialsqft_basementsqft_abovesqft_living15sqft_lotpricesqft_lot15sqft_living
plotly-logomark
0100020003000400005010015020020004000600080000200400600200040006000020040060000.5M1M1.5M010002000300001B2B3B4B05k10k15k0200k400k600k800k010002000300040005k10k0200400
Distribución inicialsqft_basementsqft_abovesqft_living15sqft_lotpricesqft_lot15sqft_living
plotly-logomark
In [36]:
# Eliminando registros identificados como outliers según el z_score

du.data = du.data[~pd.Series(z_score_outliers(du.data, 'price')[1], index=du.data.index)]
du.data = du.data[~pd.Series(z_score_outliers(du.data, 'sqft_lot')[1], index=du.data.index)]
du.data = du.data[~pd.Series(z_score_outliers(du.data, 'sqft_lot15')[1], index=du.data.index)]
In [37]:
box_plots = [plot.box(du.data, y=columna) for columna in clasificacion_columnas['numerica_continua']]
histograms = [plot.histogram(du.data, x=columna, text_auto=False) for columna in clasificacion_columnas['numerica_continua']]

plot.grid_subplot(*box_plots, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()
plot.grid_subplot(*histograms, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()
0100020003000 02000400060008000 200040006000 050k100k 02M4M6M8M 020k40k60k 05k10k
Distribución inicialsqft_basementsqft_abovesqft_living15sqft_lotpricesqft_lot15sqft_living
plotly-logomark
01000200030000501001502002000400060008000020040060020004000020040060050k100k050010002M4M6M020040060020k40k050010005k10k0200400
Distribución inicialsqft_basementsqft_abovesqft_living15sqft_lotpricesqft_lot15sqft_living
plotly-logomark

Para las variables numericas_discreta se analizarán sus rangos

In [38]:
pd.DataFrame({
    'min':du.data[clasificacion_columnas['numerica_discreta']].min(),
    'max':du.data[clasificacion_columnas['numerica_discreta']].max(),
    'nulos':du.data[clasificacion_columnas['numerica_discreta']].isna().sum()
})
Out[38]:
min max nulos
bathrooms 0.0 8.0 0
bedrooms 0.0 33.0 0
yr_renovated 1934.0 2015.0 15905
yr_built 1900.0 2015.0 0
jhygtf 0.0 2015.0 0
yr_date 2014.0 2015.0 0
antiguedad_venta -1.0 115.0 0
floors 1.0 3.0 0

Definicion de outliers:

Se considerarán outliers:

  • Apartamentos con bathrooms o bedrooms igual a 0
  • Apartamentos con bedrooms mayor a 5
  • Apartamentos con bathrooms mayor a 4
In [39]:
bathrooms_outliers = (du.data['bathrooms']==0) | (du.data['bathrooms'] > 4)
bedrooms_outliers = (du.data['bedrooms']==0) | (du.data['bedrooms'] > 5)
son_outliers = bathrooms_outliers | bedrooms_outliers
cant_outliers = son_outliers.sum()
print(f'Con estas características hay: {cant_outliers} outliers representando el {cant_outliers/len(son_outliers):.2%}')
Con estas características hay: 324 outliers representando el 1.96%

Imputación de outliers: Se reemplazarán los outliers calculando la mediana recortada la cual se realiza teniendo en cuenta únicamente los inliers

In [40]:
du.data = mediana_recortada_imputacion(du.data, 'bathrooms', bathrooms_outliers)
du.data = mediana_recortada_imputacion(du.data, 'bedrooms', bedrooms_outliers)
In [41]:
# Revisando los datos después de la imputación
plot.grid_subplot(
    *[plot.bar(du.data, x=column, max_bins=10) for column in clasificacion_columnas['numerica_discreta']], 
    cols=3, 
    titles=clasificacion_columnas['numerica_discreta']).show()
pd.DataFrame({
    'min':du.data[clasificacion_columnas['numerica_discreta']].min(),
    'max':du.data[clasificacion_columnas['numerica_discreta']].max(),
    'nulos':du.data[clasificacion_columnas['numerica_discreta']].isna().sum()
})
020004000600080002.01.03.04.0020004000600080003.04.02.05.01.0050100150200(2006.9, 2015.0](1982.6, 1990.7](1974.5, 1982.6](1950.2, 1958.3](1942.1, 1950.2]010002000(2003.5, 2015.0](1980.5, 1992.0](1992.0, 2003.5](1934.5, 1946.0](1911.5, 1923.0]05k10k15k(-2.015, 201.5](201.5, 403.0](604.5, 806.0](1007.5, 1209.0](1410.5, 1612.0]05k10k2014.02015.0010002000(-1.116, 10.6](10.6, 22.2](57.0, 68.6](68.6, 80.2](91.8, 103.4]05k10k1.02.03.0
bathroomsbedroomsyr_renovatedyr_builtjhygtfyr_dateantiguedad_ventafloors
plotly-logomark
Out[41]:
min max nulos
bathrooms 1.0 4.0 0
bedrooms 1.0 5.0 0
yr_renovated 1934.0 2015.0 15905
yr_built 1900.0 2015.0 0
jhygtf 0.0 2015.0 0
yr_date 2014.0 2015.0 0
antiguedad_venta -1.0 115.0 0
floors 1.0 3.0 0

Para las columnas categóricas se revisarán sus valores únicos.

In [42]:
pd.DataFrame({
    'distinct_count':du.data[clasificacion_columnas['categorica_ordinal']].nunique(),
    'nulos':du.data[clasificacion_columnas['categorica_ordinal']].isna().sum()
})
Out[42]:
distinct_count nulos
zipcode 70 0
grade 12 0
view 5 0
waterfront 2 0
condition 5 0
lat 429 0
long 608 0

No se evidencian datos atípicos. lat, long y zipcode tienen demasiada cardinalidad pero es normal por ser datos de ubicación.

Eliminar columnas de datos Innecesarios¶

Se realizará un perfilado de los datos para identificar problemas de calidad no identificados anteriormente.

In [43]:
profiler = ProfileReport(du.data, explorative=True)

profiler_to_file(profiler, '2_0_0 Perfilado inicial.html')
Ejecutando profiler
Summarize dataset:   0%|          | 0/5 [00:00<?, ?it/s]
Generate report structure:   0%|          | 0/1 [00:00<?, ?it/s]
Render HTML:   0%|          | 0/1 [00:00<?, ?it/s]
Export report to file:   0%|          | 0/1 [00:00<?, ?it/s]
Out[43]:
True
In [44]:
# Se utilizará el índice de correlación phik pues este permite calcular la correlación entre variables numéricas y categóricas al tiempo.
# Este indice funciona mejor cuando no hay valores nulos, por lo tanto se reemplazarán los valores nulos por 0
cor_mat = du.data.fillna(0).phik_matrix()
interval columns not set, guessing: ['zipcode', 'grade', 'sqft_basement', 'view', 'bathrooms', 'bedrooms', 'sqft_above', 'sqft_living15', 'lat', 'waterfront', 'floors', 'yr_renovated', 'yr_built', 'long', 'jhygtf', 'sqft_lot', 'price', 'condition', 'sqft_lot15', 'sqft_living', 'tiene_sotano', 'fue_renovada', 'yr_date', 'antiguedad_venta']
In [45]:
# Primero se analizará la correlación entre las variables de entrada y se descartarán aquellas con una correlación superior a 0.9

(cor_mat.loc[columnas_entrada, columnas_entrada] > 0.9).sum().sort_values(ascending=False)
Out[45]:
yr_renovated        3
fue_renovada        3
jhygtf              3
yr_built            2
tiene_sotano        2
sqft_living         2
antiguedad_venta    2
sqft_above          2
sqft_basement       2
sqft_living15       1
lat                 1
waterfront          1
floors              1
grade               1
long                1
bedrooms            1
sqft_lot            1
condition           1
sqft_lot15          1
bathrooms           1
view                1
yr_date             1
zipcode             1
dtype: int64
In [46]:
# Se analizarán con mas detalle aquellas que tienen una alta correlación con más de 1 columna (Consigo misma)
columnas_alta_correlacion = ['yr_renovated', 'fue_renovada', 'jhygtf', 'yr_built', 'tiene_sotano', 'sqft_living', 'antiguedad_venta', 'sqft_above', 'sqft_basement']
px.imshow(
    cor_mat.loc[columnas_alta_correlacion, columnas_alta_correlacion].round(2), 
    color_continuous_scale= 'blues', 
    text_auto=True).show()
1110.280.070.090.280.060.081110.280.070.090.280.060.081110.280.070.090.280.060.080.280.280.2810.350.3810.520.320.070.070.070.3510.330.350.330.970.090.090.090.380.3310.380.940.880.280.280.2810.350.3810.520.320.060.060.060.520.330.940.5210.610.080.080.080.320.970.880.320.611yr_renovatedfue_renovadajhygtfyr_builttiene_sotanosqft_livingantiguedad_ventasqft_abovesqft_basementsqft_basementsqft_aboveantiguedad_ventasqft_livingtiene_sotanoyr_builtjhygtffue_renovadayr_renovated
0.10.20.30.40.50.60.70.80.91
plotly-logomark

La primera columna a eliminar es jhygtf se observa que es la misma variable que yr_renovated. Aunque la columna fue_renovada está calculada con base en yr_renovated se conservarán ambas para posteriormente elegir con cual de las dos se puede obtener un mejor modelo.

Quedan altas correlaciones entre las siguientes columnas:

  • yr_build y antiguedad_venta
  • tiene_sotano y sqft_basement
  • sqft_living y sqft_above
  • sqft_living y sqft_basement

Para las restantes, se conservarán todas para después mediante el cálculo de la importancia de variables escoger cual tiene un mejor poder predictivo.

Entonces solo se eliminará la columna jhygtf

In [47]:
du.data = du.data.drop(columns=['jhygtf'])
In [48]:
# Correlación de todas las variables de entrada con respecto a la salida
cor_mat.loc['price', du.data.columns].sort_values(ascending=False)
Out[48]:
price               1.000000
sqft_living         0.849294
sqft_above          0.705247
sqft_basement       0.700367
grade               0.520938
sqft_living15       0.412334
bathrooms           0.397703
waterfront          0.335567
view                0.324725
floors              0.269777
sqft_lot15          0.200437
bedrooms            0.191491
sqft_lot            0.144752
zipcode             0.130665
fue_renovada        0.114474
yr_renovated        0.114474
tiene_sotano        0.107443
antiguedad_venta    0.070706
yr_built            0.069420
condition           0.042359
long                0.006108
lat                 0.000000
yr_date                  NaN
Name: price, dtype: float64

Selección de variables¶

Se calculará la importancia de las variables de entrada con respecto al precio adicionando una columna dummy que contrendrá valores aleatorios y se eliminarán aquellas variables cuya importancia sea inferior a la columna aleatoria (Al ser esta aleatoria, sabemos desde el principio que esta no puede ser una buena predictora).

Para realizar este calculo se utilizará un regresor lineal, se debe escalar primero las variables de entrada para poder comparar sus coeficientes.

In [49]:
pipeline_features = make_pipeline(StandardScaler(), LinearRegression())
X = du.data.fillna(0).drop(columns='price')
X['random'] = np.random.normal(size=(X.shape[0], 1))
y = du.data['price']

cv_model = cross_validate(
   pipeline_features, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
   return_estimator=True, n_jobs=2
)

coefs = pd.DataFrame(
   [model[1].coef_ for model in cv_model['estimator']],
   columns=X.columns
)
print()
print(f'mean_test_score: {np.mean(cv_model["test_score"])}')
print(f'std_test_score : {np.std(cv_model["test_score"])}')
mean_test_score: 0.6214407737722989
std_test_score : 0.01497525767408363
In [50]:
coefs
Out[50]:
zipcode grade sqft_basement view bathrooms bedrooms sqft_above sqft_living15 lat waterfront ... long sqft_lot condition sqft_lot15 sqft_living tiene_sotano fue_renovada yr_date antiguedad_venta random
0 -928.849420 127911.063989 -6.816505e+15 21764.050490 18562.852166 -33535.352087 -1.226075e+16 26147.192583 -1566.817927 55822.549942 ... 1164.492424 -319.083534 16310.746474 -17700.577551 1.353284e+16 4034.108721 -1.024760e+06 -2.846674e+16 1.785247e+18 1644.640830
1 -714.470424 121848.585009 -9.217886e+15 21925.745502 12831.434399 -35779.143342 -1.663493e+16 26116.859636 -896.963165 50781.893791 ... 590.501108 2067.988870 16807.804131 -21561.852653 1.831146e+16 7045.712907 -1.074064e+06 -9.500696e+12 5.954164e+14 -1174.480089
2 -2812.528204 127427.710260 2.599369e+04 27469.880684 16752.555675 -32696.696563 5.430017e+04 26200.188408 -296.850982 38672.361841 ... -43.337686 -11.834184 17648.004278 -18599.128857 6.260284e+04 9066.960447 -1.185298e+06 8.804761e+03 4.629921e+04 1397.522062
3 -1800.803810 127439.434106 4.155630e+17 23472.361688 16006.694716 -36995.768156 7.544863e+17 21780.614846 -660.783943 51985.013475 ... -523.680705 3618.438229 16127.161013 -22834.763044 -8.317639e+17 6411.516710 -1.022589e+06 -1.373314e+15 8.683376e+16 1136.302451
4 -1030.333390 127356.386421 -5.457488e+16 26074.120365 12918.508967 -35987.814267 -9.871722e+16 20099.888336 -1833.790627 53413.953926 ... 1189.824235 -1615.490127 17116.032711 -17844.794544 1.088764e+17 7405.514338 -9.442235e+05 -1.236855e+16 7.749781e+17 992.394278
5 -2216.117643 126061.003727 1.705394e+16 26651.564133 18172.994020 -36586.363667 3.064624e+16 23022.486566 -522.579869 54541.627745 ... 1325.366516 1557.688254 17518.994051 -21344.930278 -3.381665e+16 6316.096928 -1.231295e+06 -4.932653e+15 3.116065e+17 484.050579
6 -2768.984450 126732.720084 8.798511e+16 23790.333113 14962.330271 -33847.603210 1.585937e+17 27272.911168 -806.475738 45254.546531 ... -1114.635771 2319.312912 16814.681043 -19228.063129 -1.744410e+17 6470.884857 -8.103338e+05 -7.304742e+13 4.579920e+15 1720.788299
7 316.283344 125913.836172 1.893366e+17 22351.003338 13714.404167 -33694.985034 3.440806e+17 24593.385686 -1966.689701 45578.278950 ... 2170.588842 3029.593472 18139.224348 -22457.019945 -3.776510e+17 4933.863389 -9.298487e+05 -1.318006e+14 8.315558e+15 340.368479
8 -1607.384227 127452.329407 5.240559e+17 23222.018038 15946.453687 -33467.802338 9.389098e+17 23452.832950 -158.339158 52396.258469 ... 369.507031 -2481.260400 15729.161945 -16325.753114 -1.034932e+18 8332.658306 -1.156012e+06 -9.414543e+15 5.873869e+17 751.039117
9 -1073.331328 125079.437706 3.501386e+17 24608.863813 15183.938738 -37669.921174 6.356924e+17 21921.124482 -1761.619703 53343.366908 ... 177.310515 -304.430198 15482.036514 -19481.205831 -7.026013e+17 8152.153794 -1.139108e+06 -2.108399e+14 1.322500e+16 1050.661550
10 -1573.831310 125509.295107 -4.777925e+17 27510.250748 18766.298303 -34708.592071 -8.656818e+17 25163.039962 -1767.823299 42968.660251 ... 791.803964 -22.170483 16842.019553 -19421.111828 9.503048e+17 8604.398252 -8.583536e+05 2.918965e+14 -1.835426e+16 1214.277821
11 -1625.864453 126086.386710 -6.287297e+17 23120.158922 12116.716990 -35713.889585 -1.147516e+18 21305.687957 -442.342878 48304.393709 ... -271.465928 512.132469 18486.103717 -19566.213445 1.260138e+18 6882.378848 -1.237401e+06 1.498788e+15 -9.405432e+16 -1436.728216
12 -425.114363 127960.003299 2.768217e+04 22095.752574 18086.367693 -32583.591832 5.720719e+04 27296.244284 137.025176 56474.030389 ... 913.333366 4023.317676 17449.517906 -23002.402732 6.576094e+04 9101.562248 -1.114242e+06 1.083153e+04 4.671939e+04 1772.798008
13 -2423.489226 125509.259394 3.580909e+04 23087.416693 14881.073450 -37445.199150 6.018348e+04 22824.544843 -1170.732620 48690.075253 ... 341.110157 -1892.084714 14967.951478 -16277.490141 7.259350e+04 5351.287032 -1.121502e+06 9.166875e+03 4.884093e+04 1586.878231
14 -1392.797601 124720.689695 3.574875e+04 24016.574250 13531.006460 -35304.634545 5.809826e+04 24279.458952 -1547.341730 54820.246505 ... 667.661493 1160.002721 15899.790641 -20006.128526 7.062126e+04 3743.182803 -8.930083e+05 1.006998e+04 4.686217e+04 1530.956024
15 -2908.743843 123397.046494 2.590062e+18 26266.403695 20560.544071 -29001.221976 4.657491e+18 27135.412678 -1662.820529 50517.524155 ... 328.397750 101.059992 16569.827593 -17116.681673 -5.124114e+18 8266.921726 -1.267322e+06 -2.093479e+15 1.313977e+17 1549.016677
16 -852.366118 124510.167909 1.484418e+18 24024.198909 18258.395084 -37883.848081 2.682789e+18 24822.885723 -1665.850191 44949.066574 ... 291.750644 1674.342308 15631.548548 -19915.927571 -2.951772e+18 7366.282944 -8.973885e+05 3.306861e+14 -2.083475e+16 -972.293456
17 -1675.717135 130191.090425 -3.175311e+17 21776.767767 11624.179142 -36873.673162 -5.785848e+17 24120.293436 -258.488291 55524.785853 ... 365.923967 828.569763 17816.943474 -21934.200926 6.369843e+17 6279.746241 -1.064024e+06 -3.484648e+14 2.197829e+16 236.204573
18 -1226.393532 127689.929239 1.269115e+18 26306.892307 16442.700035 -34195.082588 2.278255e+18 24098.767777 -843.364732 49351.362006 ... -663.655001 533.133992 17675.570226 -18390.300326 -2.512021e+18 6647.712080 -9.912322e+05 -3.220481e+15 2.020717e+17 3001.136510
19 -1371.581520 127115.413956 -1.140197e+16 23045.334414 13089.689554 -36630.991307 -2.053732e+16 20600.034599 -403.065697 49822.692225 ... 365.117461 2151.010547 16520.716764 -21839.303150 2.268144e+16 3435.860426 -1.112920e+06 -1.366462e+13 8.537728e+14 1117.122001
20 -1260.971953 126191.862777 5.197256e+17 24539.111413 17433.842626 -34962.376872 9.351875e+17 23934.835455 -1052.514806 53613.968389 ... 734.510733 1537.288447 16964.710124 -20692.908677 -1.030593e+18 6196.449917 -1.143419e+06 -8.084381e+14 5.053966e+16 2585.446972
21 -2497.732855 128874.033819 3.156448e+04 22361.614047 13832.745967 -36844.661170 6.301624e+04 19164.540833 -367.554579 53295.157585 ... 1313.053486 -743.774658 16658.608568 -18110.698286 7.284091e+04 7989.497792 -9.300871e+05 1.039730e+04 4.763030e+04 -102.755138
22 -1045.828477 127217.138240 1.896872e+18 26067.926634 13414.255697 -34540.235009 3.431363e+18 25907.374172 -1680.044077 50682.694944 ... 2165.687390 1865.477780 16869.828708 -21654.269985 -3.772668e+18 5474.368234 -8.008335e+05 -2.563891e+14 1.615678e+16 1464.077725
23 -2273.995789 123078.628472 -9.420465e+17 25832.367831 13979.223908 -34845.646345 -1.699607e+18 22747.351363 -2133.183683 45366.484451 ... 395.999894 1403.161789 16381.193068 -21151.766797 1.879056e+18 5004.897824 -1.048460e+06 2.620299e+15 -1.640383e+17 -862.690662
24 -2785.093771 125208.514953 2.408115e+16 21477.992564 18480.121447 -34342.835451 4.369470e+16 28327.531385 -663.943784 48368.836141 ... -1698.430768 654.849008 16306.960533 -18646.232498 -4.785104e+16 8917.854051 -1.282027e+06 5.195015e+16 -3.266346e+18 183.098308

25 rows × 23 columns

In [51]:
px.box(coefs, orientation='h', title='Importancia de los coeficientes y sus variaciones')
−4×10​18​−2×10​18​02×10​18​4×10​18​zipcodegradesqft_basementviewbathroomsbedroomssqft_abovesqft_living15latwaterfrontfloorsyr_renovatedyr_builtlongsqft_lotconditionsqft_lot15sqft_livingtiene_sotanofue_renovadayr_dateantiguedad_ventarandom
Importancia de los coeficientes y sus variacionesvaluevariable
plotly-logomark
In [52]:
# Debido a que no se normalizó el precio, se dividirán los coeficientes por la media del precio para tenerlos en una escala mas
# manejable

coefs_resumen = pd.DataFrame({
    'variacion': (coefs.std()/du.data['price'].mean()),
    'media': coefs.mean()/du.data['price'].mean(),
    'media_absoluta': coefs.abs().mean()/du.data['price'].mean()
})
coef_variacion = coefs_resumen['variacion'].sort_values(ascending=False)
coef_variacion
Out[52]:
sqft_living         3.042883e+12
sqft_above          2.764742e+12
antiguedad_venta    1.548154e+12
yr_built            1.548112e+12
sqft_basement       1.532987e+12
yr_date             2.464350e+10
yr_renovated        2.770194e-01
fue_renovada        2.764609e-01
waterfront          8.773718e-03
bathrooms           4.827334e-03
sqft_living15       4.744334e-03
bedrooms            3.910513e-03
sqft_lot15          3.896441e-03
floors              3.759966e-03
view                3.733161e-03
grade               3.668911e-03
tiene_sotano        3.258471e-03
sqft_lot            3.176893e-03
random              2.193772e-03
long                1.747957e-03
condition           1.688643e-03
zipcode             1.620135e-03
lat                 1.306657e-03
Name: variacion, dtype: float64

Para aquellas variables con demasiada variación no es confiable tomar el promedio como su importancia, por lo tanto no serán eliminadas.

Se analizarán aquellas con una variación inferior a 0.7

In [53]:
coefs_confiables = coef_variacion[coef_variacion <= 0.7]
coefs_confiables
Out[53]:
yr_renovated     0.277019
fue_renovada     0.276461
waterfront       0.008774
bathrooms        0.004827
sqft_living15    0.004744
bedrooms         0.003911
sqft_lot15       0.003896
floors           0.003760
view             0.003733
grade            0.003669
tiene_sotano     0.003258
sqft_lot         0.003177
random           0.002194
long             0.001748
condition        0.001689
zipcode          0.001620
lat              0.001307
Name: variacion, dtype: float64
In [54]:
px.box(coefs[coefs_confiables.index], orientation='h', title='Importancia de los coeficientes y sus variaciones').show()
px.box(coefs.abs()[coefs_confiables.index], orientation='h', title='Importancia absoluta de los coeficientes y sus variaciones').show()
050k100kyr_renovatedfue_renovadawaterfrontbathroomssqft_living15bedroomssqft_lot15floorsviewgradetiene_sotanosqft_lotrandomlongconditionzipcodelat
Importancia de los coeficientes y sus variacionesvaluevariable
plotly-logomark
00.5M1Myr_renovatedfue_renovadawaterfrontbathroomssqft_living15bedroomssqft_lot15floorsviewgradetiene_sotanosqft_lotrandomlongconditionzipcodelat
Importancia absoluta de los coeficientes y sus variacionesvaluevariable
plotly-logomark

De esta forma se pueden eliminar aquellas que sean consistentemente peor que la columna random.

Al analizar el gráfico, no se pueden identificar variables con un peor desempeño que random.

Se usará f_regression de scikit learn para calcular la importancia de las variables.

In [55]:
from sklearn.feature_selection import f_regression

f_statistic, p_values =  f_regression(X, y)
pd.DataFrame({'f_statistic': f_statistic, 'p_value': p_values.round(3)}, index=X.columns).sort_values(by='f_statistic', ascending=False)
Out[55]:
f_statistic p_value
sqft_living 13633.682142 0.000
grade 11295.852987 0.000
sqft_above 7942.786595 0.000
sqft_living15 7622.345514 0.000
bathrooms 3727.835576 0.000
view 2569.742164 0.000
sqft_basement 1741.583489 0.000
bedrooms 1368.668278 0.000
waterfront 1276.563745 0.000
floors 842.686506 0.000
tiene_sotano 509.942279 0.000
sqft_lot15 363.784440 0.000
sqft_lot 331.488959 0.000
yr_renovated 224.215157 0.000
fue_renovada 223.038145 0.000
zipcode 36.987037 0.000
condition 35.840032 0.000
antiguedad_venta 34.136911 0.000
yr_built 34.030063 0.000
lat 0.912105 0.340
long 0.479515 0.489
yr_date 0.350826 0.554
random 0.348072 0.555

Con esta técnica, se puede determinar el umbral desde el que se van a descartar las variables, se observa que ninguna variable tiene un poder predictivo peor que la variable random.

Se puede ver que la importancia de las variables lat, long y yr_date no son confiables al tener un p-value mayor a 0.05

Adicionalmente, podemos determinar que de las variables de entrada altamente correlacionadas, se pueden eliminar las siguientes

  • antiguedad_venta > yr_built
  • sqft_basement > tiene_sotano
  • sqft_living > sqft_above
  • sqft_living > sqft_basement

Para yr_renovated y fue_renovada la diferencia es muy poca, por lo tanto se escogerá fue_renovada por la alta cantidad de nulos de yr_renovated

Por lo tanto se descartarán las siguientes columnas:

lat, long, yr_date, yr_built, tiene_sotano, sqft_above, sqft_basement, yr_built y yr_renovated

In [56]:
du.data = du.data.drop(columns=['lat', 'long', 'yr_date', 'yr_built', 'tiene_sotano', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated'])

Procesamiento de Datos Faltantes¶

Ya que se eliminaron las columnas yr_renovated y sqft_basement, se puede omitir este paso pues ya no quedan mas nulos.

In [57]:
du.data.isnull().sum().sort_values(ascending=False)
Out[57]:
zipcode             0
grade               0
view                0
bathrooms           0
bedrooms            0
sqft_living15       0
waterfront          0
floors              0
sqft_lot            0
price               0
condition           0
sqft_lot15          0
sqft_living         0
fue_renovada        0
antiguedad_venta    0
dtype: int64

Analisis Univariable¶

Estadistico Descriptico y Analisis

Variables Numericas¶

| Tendencia Central | Medida de Dispersión | Visualizacion | |:-----------------:|:-------------------------:|:-------------:| | Media | Rango | Histogramas | | Mediana | Cuartiles | Boxplots | | Moda | Rango inter cuartil (IQR) | | | Minimo | Varianza | | | Maximo | Desviacion Estandard | | | . | Skewness | | | . | Kurtosis | |

In [58]:
calcular_descriptivas(du.data)
Out[58]:
zipcode grade view bathrooms bedrooms sqft_living15 waterfront floors sqft_lot price condition sqft_lot15 sqft_living fue_renovada antiguedad_venta
count 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 1.655400e+04 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000
mean 98079.010269 7.583968 0.200254 1.713121 3.306572 1944.150840 0.005739 1.436873 9934.879667 5.106034e+05 3.408058 8996.247856 2016.595143 0.039205 43.645524
std 53.580311 1.103965 0.704138 0.674940 0.821211 649.237427 0.075539 0.552370 10957.363161 3.238058e+05 0.648368 7636.814694 848.705398 0.194088 29.345804
min 98001.000000 1.000000 0.000000 1.000000 1.000000 460.000000 0.000000 1.000000 520.000000 7.500000e+04 1.000000 659.000000 290.000000 0.000000 -1.000000
25% 98033.000000 7.000000 0.000000 1.000000 3.000000 1470.000000 0.000000 1.000000 5000.000000 3.160000e+05 3.000000 5011.250000 1400.000000 0.000000 18.000000
50% 98070.000000 7.000000 0.000000 2.000000 3.000000 1810.000000 0.000000 1.000000 7480.000000 4.400000e+05 3.000000 7500.000000 1880.000000 0.000000 40.000000
75% 98118.000000 8.000000 0.000000 2.000000 4.000000 2300.000000 0.000000 2.000000 10140.000000 6.200000e+05 4.000000 9750.000000 2478.750000 0.000000 63.000000
max 98199.000000 13.000000 4.000000 4.000000 5.000000 5790.000000 1.000000 3.000000 137214.000000 7.700000e+06 5.000000 57140.000000 12050.000000 1.000000 115.000000
rango 198.000000 12.000000 4.000000 3.000000 4.000000 5330.000000 1.000000 2.000000 136694.000000 7.625000e+06 4.000000 56481.000000 11760.000000 1.000000 116.000000
IQR 85.000000 1.000000 0.000000 1.000000 1.000000 830.000000 0.000000 1.000000 5140.000000 3.040000e+05 1.000000 4738.750000 1078.750000 0.000000 45.000000
coef de var 0.000546 0.145566 3.516232 0.393982 0.248357 0.333944 13.162944 0.384425 1.102919 6.341630e-01 0.190246 0.848889 0.420861 4.950597 0.672367
skewness 0.372547 0.716523 3.707596 0.644071 0.045938 1.059620 13.087759 0.776642 4.698177 4.658301e+00 1.038248 3.168057 1.323768 4.748876 0.448007
kurtosis -0.884930 1.371933 13.379400 0.237942 -0.103048 1.455089 169.309900 -0.465117 31.240935 5.059510e+01 0.540616 11.847458 4.451355 20.554309 -0.669613
In [59]:
# Actualizando la clasificación de columnas para dejar solo las que están en el dataframe
clasificacion_columnas['numerica_continua'] = list(set(clasificacion_columnas['numerica_continua']).intersection(du.data.columns))
clasificacion_columnas['categorica_ordinal'] = list(set(clasificacion_columnas['categorica_ordinal']).intersection(du.data.columns))
clasificacion_columnas['numerica_discreta'] = list(set(clasificacion_columnas['numerica_discreta']).intersection(du.data.columns))


columnas_a_graficar = clasificacion_columnas['numerica_continua']
In [60]:
plots1 = [plot.box(du.data, y=variable_numerica) for variable_numerica in columnas_a_graficar] 
plot.grid_subplot(*plots1, cols=3, title='Diagramas de cajas y bigotes', titles=columnas_a_graficar).show()

plots2 = [plot.histogram(du.data, x=variable_numerica) for variable_numerica in columnas_a_graficar]
plot.grid_subplot(*plots2, cols=3, title='Histogramas', titles=columnas_a_graficar).show()
020k40k60k 200040006000 05k10k 050k100k 02M4M6M8M
Diagramas de cajas y bigotessqft_lot15sqft_living15sqft_livingsqft_lotprice
plotly-logomark
37524305157200323544918772121365590157110591221116979558977257238728823223614611110813186123617048434047303244452324332019161115191114231011101211128117471591181222734235241720161210181419118613112710108851064103710681161234972128420k40k05001000123101032476711316820225933139046454354563262669266462160956054954548746846443443134936833933129526830125824327021619818517515713613015712090809898665659555746355329261923242820131613974103109233315221211115200040000200400600124510192847114118172168229231327295300349395408430384487443443443429473423412442375389400381398380366332288321283326243298236237214228193191140154148160120118138120105838481747164565063355140403324302925213220192020101311108638865562274254332534121212111211221211121115k10k02004001635352712002714095418017731053612918631113098095466060372250835127123124919616713211995151969071575464574350462845353423162219231218181424142414138157912161891312196825232419151812101419201391081329853175101541084899469565155352394222535312123211231311111311121226211111611311221111211116113411111112121111131211111112150k100k0500100022027519918225640750365569563968774472762861762268662350249143849549041534935232033028326525424121221717916616314812811397104908276321544161534132411142912311112208167521716181439481861228112622525168824621512439251474114121214121121421211432212111111112M4M6M0200400600
Histogramassqft_lot15sqft_living15sqft_livingsqft_lotprice
plotly-logomark

Se observan datos asimétricos para todas las columnas numéricas.

Variables Categoricas¶

  • Numero de elementos por categoria
  • Porcentaje de elementos por categoria
  • Graficos de barras
In [61]:
resumen = []

for variable_categorica in clasificacion_columnas['categorica_ordinal']:
    col = du.data[variable_categorica]
    elms_cat = col.groupby(by=col).agg('count')
    total = elms_cat.sum()
    porc = elms_cat / total
    porc.name = 'porc'
    df = pd.DataFrame([elms_cat, porc]).transpose()
    resumen.append(df)

for tabla in resumen:
    display(HTML(tabla.to_html()))
    variable = tabla.columns[0]
    fig = px.bar(tabla[variable], orientation='h', title=str(variable))
    fig.show()
view porc
view
0 15123.0 0.913556
1 251.0 0.015162
2 666.0 0.040232
3 324.0 0.019572
4 190.0 0.011478
05k10k15k01234
variableviewviewvalueindex
plotly-logomark
condition porc
condition
1 23.0 0.001389
2 126.0 0.007611
3 10763.0 0.650175
4 4357.0 0.263199
5 1285.0 0.077625
02k4k6k8k10k12345
variableconditionconditionvalueindex
plotly-logomark
zipcode porc
zipcode
98001 291.0 0.017579
98002 160.0 0.009665
98003 213.0 0.012867
98004 205.0 0.012384
98005 132.0 0.007974
98006 377.0 0.022774
98007 113.0 0.006826
98008 226.0 0.013652
98010 69.0 0.004168
98011 150.0 0.009061
98014 65.0 0.003927
98019 131.0 0.007913
98022 140.0 0.008457
98023 399.0 0.024103
98024 31.0 0.001873
98027 265.0 0.016008
98028 220.0 0.013290
98029 268.0 0.016189
98030 198.0 0.011961
98031 219.0 0.013229
98032 106.0 0.006403
98033 307.0 0.018545
98034 446.0 0.026942
98038 431.0 0.026036
98039 29.0 0.001752
98040 187.0 0.011296
98042 431.0 0.026036
98045 150.0 0.009061
98052 467.0 0.028211
98053 287.0 0.017337
98055 221.0 0.013350
98056 322.0 0.019451
98058 361.0 0.021807
98059 381.0 0.023016
98065 238.0 0.014377
98070 54.0 0.003262
98072 216.0 0.013048
98074 336.0 0.020297
98075 274.0 0.016552
98077 131.0 0.007913
98092 250.0 0.015102
98102 88.0 0.005316
98103 477.0 0.028815
98105 166.0 0.010028
98106 275.0 0.016612
98107 211.0 0.012746
98108 153.0 0.009242
98109 85.0 0.005135
98112 166.0 0.010028
98115 476.0 0.028754
98116 259.0 0.015646
98117 442.0 0.026700
98118 406.0 0.024526
98119 141.0 0.008518
98122 209.0 0.012625
98125 331.0 0.019995
98126 273.0 0.016491
98133 402.0 0.024284
98136 209.0 0.012625
98144 254.0 0.015344
98146 228.0 0.013773
98148 51.0 0.003081
98155 372.0 0.022472
98166 200.0 0.012082
98168 218.0 0.013169
98177 192.0 0.011598
98178 211.0 0.012746
98188 108.0 0.006524
98198 220.0 0.013290
98199 234.0 0.014136
010020030040050098.05k98.1k98.15k
variablezipcodezipcodevalueindex
plotly-logomark
grade porc
grade
1 1.0 0.000060
3 3.0 0.000181
4 23.0 0.001389
5 183.0 0.011055
6 1592.0 0.096170
7 7145.0 0.431618
8 4770.0 0.288148
9 1881.0 0.113628
10 698.0 0.042165
11 211.0 0.012746
12 40.0 0.002416
13 7.0 0.000423
020004000600024681012
variablegradegradevalueindex
plotly-logomark
waterfront porc
waterfront
0 16459.0 0.994261
1 95.0 0.005739
05k10k15k−0.500.511.5
variablewaterfrontwaterfrontvalueindex
plotly-logomark

Analisis Bivariable¶

Estadistico Descriptico y Analisis

Numericas vs Numericas¶

  • Scatter Plot
  • Heatmap
  • Correlacion
In [62]:
columnas_a_graficar = list(filter(lambda x: x!='price', clasificacion_columnas['numerica_continua']))

plots = [plot.scatter(du.data, x=columna, y='price') for columna in columnas_a_graficar]

plot.grid_subplot(*plots, cols = 2, titles=columnas_a_graficar).show()
020k40k60k02M4M6M8M20004000600002M4M6M8M05k10k02M4M6M8M050k100k02M4M6M8M
sqft_lot15sqft_living15sqft_livingsqft_lot
plotly-logomark
In [63]:
clasificacion_columnas
Out[63]:
{'categorica_ordinal': ['view', 'condition', 'zipcode', 'grade', 'waterfront'],
 'fecha': ['date'],
 'id': ['index'],
 'numerica_continua': ['sqft_lot15',
  'sqft_living15',
  'sqft_living',
  'sqft_lot',
  'price'],
 'numerica_discreta': ['bedrooms', 'antiguedad_venta', 'bathrooms', 'floors']}
In [64]:
columnas_a_graficar = clasificacion_columnas['categorica_ordinal'] + clasificacion_columnas['numerica_discreta']
nbins = [0 if len(du.data[x].unique()) < 20 else 20 for x in columnas_a_graficar]

plots = [
    plot.box(du.data, x=column, y='price', nbins=nbin)
    for column, nbin in zip(columnas_a_graficar, nbins)
]
plot.grid_subplot(*plots, 
                  cols=2, 
                  titles=columnas_a_graficar, 
                  title='Box plot entre entradas categóricas y precio',
                  height=1500
                 ).show()
02402M4M6M8M2402M4M6M8M[98001-98011)[98043-98053)[98136-98147)[98147-98157)[98116-98126)[98095-98105)[98074-98084)[98168-98178)[98053-98064)[98178-98189)02M4M6M8M51002M4M6M8M0102M4M6M8M2402M4M6M8M[17-23)[23-30)[30-36)[5-11)[42-48)[97-103)[11-17)[91-97)[72-78)[109-115)02M4M6M8M123402M4M6M8M12302M4M6M8M
Box plot entre entradas categóricas y precioviewconditionzipcodegradewaterfrontbedroomsantiguedad_ventabathroomsfloors
plotly-logomark

De las variables categóricas, las que parecen tener un mayor impacto en el precio son grade, view y y waterfront. La variable condicion parece tener un aumento en el precio cuando es mayor a 3.

In [65]:
from pandas import DataFrame

corr_matrix: DataFrame = du.data.phik_matrix()
interval columns not set, guessing: ['zipcode', 'grade', 'view', 'bathrooms', 'bedrooms', 'sqft_living15', 'waterfront', 'floors', 'sqft_lot', 'price', 'condition', 'sqft_lot15', 'sqft_living', 'fue_renovada', 'antiguedad_venta']
In [66]:
corr_matrix['price'].sort_values(ascending=False)
Out[66]:
price               1.000000
sqft_living         0.849294
grade               0.520938
sqft_living15       0.412334
bathrooms           0.397703
waterfront          0.335567
view                0.324725
floors              0.269777
sqft_lot15          0.200437
bedrooms            0.191491
sqft_lot            0.144752
zipcode             0.130665
fue_renovada        0.114474
antiguedad_venta    0.070706
condition           0.042359
Name: price, dtype: float64

Las variables relacionadas con el tamaño del apartamento y la calificación tienen mayor correlación con el precio del apartamento.

In [67]:
# Ordenando matriz de correlación con respecto a precio
corr_matrix = corr_matrix.sort_values(by='price', ascending=False)
corr_matrix = corr_matrix.reindex(columns=corr_matrix.index)
sns.heatmap(corr_matrix, cmap='PuOr')
Out[67]:
<AxesSubplot:>
In [68]:
columnas = ['sqft_living', 'sqft_above', 'sqft_basement', 'antiguedad_venta', 'yr_built', 'yr_date', 'yr_renovated', 
            'fue_renovada', 'grade', 'sqft_living15', 'sqft_lot', 'tiene_sotano', 'condition', 'floors', 'bathrooms', 
            'view', 'bedrooms', 'waterfront', 'sqft_lot15', 'zipcode']
In [69]:
profiler2 = ProfileReport(du.data, explorative=True)
profiler_to_file(profiler2, '2_0_1 Perfilado de datos.html')
Ejecutando profiler
Summarize dataset:   0%|          | 0/5 [00:00<?, ?it/s]
Generate report structure:   0%|          | 0/1 [00:00<?, ?it/s]
Render HTML:   0%|          | 0/1 [00:00<?, ?it/s]
Export report to file:   0%|          | 0/1 [00:00<?, ?it/s]
Out[69]:
True
In [70]:
calcular_descriptivas(du.data)
Out[70]:
zipcode grade view bathrooms bedrooms sqft_living15 waterfront floors sqft_lot price condition sqft_lot15 sqft_living fue_renovada antiguedad_venta
count 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 1.655400e+04 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000
mean 98079.010269 7.583968 0.200254 1.713121 3.306572 1944.150840 0.005739 1.436873 9934.879667 5.106034e+05 3.408058 8996.247856 2016.595143 0.039205 43.645524
std 53.580311 1.103965 0.704138 0.674940 0.821211 649.237427 0.075539 0.552370 10957.363161 3.238058e+05 0.648368 7636.814694 848.705398 0.194088 29.345804
min 98001.000000 1.000000 0.000000 1.000000 1.000000 460.000000 0.000000 1.000000 520.000000 7.500000e+04 1.000000 659.000000 290.000000 0.000000 -1.000000
25% 98033.000000 7.000000 0.000000 1.000000 3.000000 1470.000000 0.000000 1.000000 5000.000000 3.160000e+05 3.000000 5011.250000 1400.000000 0.000000 18.000000
50% 98070.000000 7.000000 0.000000 2.000000 3.000000 1810.000000 0.000000 1.000000 7480.000000 4.400000e+05 3.000000 7500.000000 1880.000000 0.000000 40.000000
75% 98118.000000 8.000000 0.000000 2.000000 4.000000 2300.000000 0.000000 2.000000 10140.000000 6.200000e+05 4.000000 9750.000000 2478.750000 0.000000 63.000000
max 98199.000000 13.000000 4.000000 4.000000 5.000000 5790.000000 1.000000 3.000000 137214.000000 7.700000e+06 5.000000 57140.000000 12050.000000 1.000000 115.000000
rango 198.000000 12.000000 4.000000 3.000000 4.000000 5330.000000 1.000000 2.000000 136694.000000 7.625000e+06 4.000000 56481.000000 11760.000000 1.000000 116.000000
IQR 85.000000 1.000000 0.000000 1.000000 1.000000 830.000000 0.000000 1.000000 5140.000000 3.040000e+05 1.000000 4738.750000 1078.750000 0.000000 45.000000
coef de var 0.000546 0.145566 3.516232 0.393982 0.248357 0.333944 13.162944 0.384425 1.102919 6.341630e-01 0.190246 0.848889 0.420861 4.950597 0.672367
skewness 0.372547 0.716523 3.707596 0.644071 0.045938 1.059620 13.087759 0.776642 4.698177 4.658301e+00 1.038248 3.168057 1.323768 4.748876 0.448007
kurtosis -0.884930 1.371933 13.379400 0.237942 -0.103048 1.455089 169.309900 -0.465117 31.240935 5.059510e+01 0.540616 11.847458 4.451355 20.554309 -0.669613

Analisis Univariable y Bivariable Final¶

In [71]:
profiler3 = ProfileReport(du.data, explorative=True)
In [72]:
profiler_to_file(profiler3, '2_0_2 Transformacion_final.html')
profiler3
Ejecutando profiler
Summarize dataset:   0%|          | 0/5 [00:00<?, ?it/s]
Generate report structure:   0%|          | 0/1 [00:00<?, ?it/s]
Render HTML:   0%|          | 0/1 [00:00<?, ?it/s]
Export report to file:   0%|          | 0/1 [00:00<?, ?it/s]
Out[72]:

In [73]:
du.data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 16554 entries, 19857 to 237
Data columns (total 15 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   zipcode           16554 non-null  int32  
 1   grade             16554 non-null  int32  
 2   view              16554 non-null  int32  
 3   bathrooms         16554 non-null  float64
 4   bedrooms          16554 non-null  float64
 5   sqft_living15     16554 non-null  float64
 6   waterfront        16554 non-null  int32  
 7   floors            16554 non-null  float64
 8   sqft_lot          16554 non-null  float64
 9   price             16554 non-null  float64
 10  condition         16554 non-null  int32  
 11  sqft_lot15        16554 non-null  float64
 12  sqft_living       16554 non-null  float64
 13  fue_renovada      16554 non-null  int32  
 14  antiguedad_venta  16554 non-null  float64
dtypes: float64(9), int32(6)
memory usage: 1.6 MB

Variables de entrada¶

  • zipcode
  • grade
  • view
  • bathrooms
  • bedrooms
  • sqft_living15
  • waterfront
  • floors
  • sqft_lot
  • condition
  • sqft_lot15
  • sqft_living
  • fue_renovada
  • antiguedad_venta

Variables de salida¶

  • price

MODELAMIENTO DE LOS DATOS (MACHINE LEARNING)¶

In [74]:
variables_entrada = ['zipcode', 'grade', 'view', 'bathrooms', 'bedrooms', 'sqft_living15', 'waterfront', 'floors', 'sqft_lot', 'condition', 'sqft_lot15', 'sqft_living', 'fue_renovada', 'antiguedad_venta']
variable_salida  = 'price'
du.data = du.data[variables_entrada + [variable_salida]]
In [75]:
y_name = 'price'
x_names = [columna for columna in du.data.columns if not columna == 'price']

Validacion y Evaluacion Cruzada (k-fold Cross Validation)¶

Se hace seleccion de los mejores modelos usando el Training Set y k-fold Cross Validation

In [76]:
alpha = 1
l1_ratio=0.5
normalize=False
max_iter=100000
warm_start=True
modelos_a_probar = {
    'Linear_Regression': {'modelo': make_pipeline(StandardScaler(), LinearRegression())},
    'Linear_Regression degree 2': {'modelo': make_pipeline(
        PolynomialFeatures(degree=2),
        LinearRegression()
    )},
    'Linear_Regression degree 3': {'modelo': make_pipeline(
        PolynomialFeatures(degree=3),
        LinearRegression()
    )},
    'Linear_Regression degree 2 with normalization': {'modelo': make_pipeline(
        StandardScaler(),
        PolynomialFeatures(degree=2),
        LinearRegression()
    )},
    'Lasso': {'modelo': Lasso()},
    'Ridge': {'modelo': make_pipeline(
        StandardScaler(),
        Ridge()
    )},
    'Ridge degree 2': {'modelo': make_pipeline(
        StandardScaler(),
        PolynomialFeatures(degree=2),
        Ridge()
    )},
    'ElasticNet': {'modelo': make_pipeline(
        StandardScaler(),
        ElasticNet(alpha=alpha, l1_ratio=l1_ratio, max_iter=max_iter, warm_start=warm_start)
    )},
    'SGD': {'modelo': make_pipeline(StandardScaler(), SGDRegressor())},
    'SVR': {'modelo': make_pipeline(StandardScaler(), SVR())}
}
In [77]:
for nombre_modelo, dic_modelo in tqdm(modelos_a_probar.items(), desc='Realizando cross validation...'):
    inicial = datetime.now()
    modelo = dic_modelo['modelo']
    dic_modelo['scores'] = cross_val_score(modelo, du.data[x_names], du.data[y_name], cv=5, scoring='r2')
    tiempo_entrenamiento = (datetime.now() - inicial).total_seconds()
    dic_modelo['tiempo_entrenamiento'] = tiempo_entrenamiento
    dic_modelo['media'] = np.mean(dic_modelo['scores'])
    dic_modelo['std'] = np.std(dic_modelo['scores'])
Realizando cross validation...: 100%|██████████████████████████████████████████████████████████████████████████████| 10/10 [01:25<00:00,  8.59s/it]
In [78]:
tabla_comparativa = pd.DataFrame(modelos_a_probar).transpose()
In [79]:
print('Comparativa R2')
tabla_comparativa.drop(columns=['scores','modelo']).sort_values(by='media', ascending=False)
Comparativa R2
Out[79]:
tiempo_entrenamiento media std
Linear_Regression degree 2 with normalization 0.971336 0.721899 0.017641
Ridge degree 2 0.27502 0.721759 0.017615
Linear_Regression degree 2 1.081678 0.721753 0.017614
Ridge 0.080008 0.621266 0.015699
Linear_Regression 0.105011 0.621266 0.015701
Lasso 0.720382 0.621265 0.015702
SGD 0.25253 0.618786 0.017617
ElasticNet 0.300569 0.575868 0.01368
Linear_Regression degree 3 6.551879 0.240802 0.311652
SVR 75.562808 -0.045957 0.006827

Se escogerá el Ridge degree 2 pues aunque el regresor lineal de grado 2 con normalización obtuvo una media ligeramente mayor, el modelo Ridge de grado 2 lo superó bastante en tiempo de entrenamiento. Como segundo modelo para la optimización de hiper parámetros se utilizará Linear_Regression degree 2 with normalization

In [80]:
mejor_modelo1 = modelos_a_probar['Ridge degree 2']['modelo']
mejor_modelo2 = modelos_a_probar['Linear_Regression degree 2 with normalization']['modelo']
mejor_modelo1.fit(du.data[x_names], du.data[y_name])
Out[80]:
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('polynomialfeatures', PolynomialFeatures()),
                ('ridge', Ridge())])
In [81]:
y_predict = mejor_modelo1.predict(du.data[x_names])
In [82]:
y_real = du.data['price']
In [83]:
vector = np.linspace(y_real.min()*0.8, y_real.max()*1.2)
fig1 = plot.scatter(pd.DataFrame({'y_real': y_real, 'y_pred': y_predict}), x='y_real', y='y_pred')
fig2 = px.line(pd.DataFrame({'y_real': vector, 'y_pred': vector}), x='y_real', y='y_pred')
plot.combine_plots(fig2, fig1).show()
02M4M6M8M02M4M6M8M
plotly-logomark

Optimizacion de Hiper parametros (Hyper Parameter optimization)¶

Se seleccionan solo los mejores modelos para realizar el ajuste de hiperparametros, ya que tiene una carga computacional alta.

Al final se obtienen los parametros del mejor modelo

In [84]:
# Obteniendo el nombre de los parámetros del primer modelo
mejor_modelo1.get_params()
Out[84]:
{'memory': None,
 'steps': [('standardscaler', StandardScaler()),
  ('polynomialfeatures', PolynomialFeatures()),
  ('ridge', Ridge())],
 'verbose': False,
 'standardscaler': StandardScaler(),
 'polynomialfeatures': PolynomialFeatures(),
 'ridge': Ridge(),
 'standardscaler__copy': True,
 'standardscaler__with_mean': True,
 'standardscaler__with_std': True,
 'polynomialfeatures__degree': 2,
 'polynomialfeatures__include_bias': True,
 'polynomialfeatures__interaction_only': False,
 'polynomialfeatures__order': 'C',
 'ridge__alpha': 1.0,
 'ridge__copy_X': True,
 'ridge__fit_intercept': True,
 'ridge__max_iter': None,
 'ridge__normalize': 'deprecated',
 'ridge__positive': False,
 'ridge__random_state': None,
 'ridge__solver': 'auto',
 'ridge__tol': 0.001}
In [85]:
# Obteniendo el nombre de los parámetros del segundo modelo
mejor_modelo2.get_params()
Out[85]:
{'memory': None,
 'steps': [('standardscaler', StandardScaler()),
  ('polynomialfeatures', PolynomialFeatures()),
  ('linearregression', LinearRegression())],
 'verbose': False,
 'standardscaler': StandardScaler(),
 'polynomialfeatures': PolynomialFeatures(),
 'linearregression': LinearRegression(),
 'standardscaler__copy': True,
 'standardscaler__with_mean': True,
 'standardscaler__with_std': True,
 'polynomialfeatures__degree': 2,
 'polynomialfeatures__include_bias': True,
 'polynomialfeatures__interaction_only': False,
 'polynomialfeatures__order': 'C',
 'linearregression__copy_X': True,
 'linearregression__fit_intercept': True,
 'linearregression__n_jobs': None,
 'linearregression__normalize': 'deprecated',
 'linearregression__positive': False}
In [86]:
model1_param_grid = [
    {
        'polynomialfeatures__interaction_only': [True, False],
        'ridge__alpha': np.linspace(0.01, 6, 60)
    }
]

model2_param_grid = [
    {
        'polynomialfeatures__interaction_only': [True, False],
        'polynomialfeatures__include_bias': [True, False],
        'linearregression__positive': [True, False]
    }
]
In [87]:
from sklearn.model_selection import GridSearchCV

gs1 = GridSearchCV(mejor_modelo1, model1_param_grid, scoring='r2')
gs2 = GridSearchCV(mejor_modelo2, model2_param_grid, scoring='r2')
In [88]:
# Realizando un grid search para el primer pipeline
print('Grid search primer modelo')
gs1.fit(du.data[x_names], du.data[y_name])

print('Grid search segundo modelo')
# Realizando un grid search para el segundo pipeline
gs2.fit(du.data[x_names], du.data[y_name])
print('Listo')
Grid search primer modelo
Grid search segundo modelo
Listo
In [89]:
print('Los mejores parámetros para el primer pipeline son:')
print(gs1.best_params_)
print()
print('Los mejores parámetros para el segundo pipeline son:')
print(gs2.best_params_)
Los mejores parámetros para el primer pipeline son:
{'polynomialfeatures__interaction_only': False, 'ridge__alpha': 6.0}

Los mejores parámetros para el segundo pipeline son:
{'linearregression__positive': False, 'polynomialfeatures__include_bias': True, 'polynomialfeatures__interaction_only': False}
In [90]:
from sklearn.compose import make_column_transformer

modelo_optimizado = make_pipeline(
        make_column_transformer(
            ('passthrough', [
                'zipcode', 'grade', 'view', 'bathrooms', 'bedrooms', 'sqft_living15', 'waterfront', 'floors',
                'sqft_lot', 'condition', 'sqft_lot15', 'sqft_living', 'fue_renovada', 'antiguedad_venta'
            ])
        ),
        StandardScaler(),
        PolynomialFeatures(degree=2, interaction_only=False),
        Ridge(alpha=6.0)
    )

Evaluacion final del modelo con el Test set¶

Después de haber obtenido el flujo para la transformación de los datos, se empaquetó en un conjunto de pipelines de transformación.

In [91]:
set_validacion = du.load_data(du.raw_validation_path)
set_entrenamiento = du.load_data(du.raw_train_test_path)
In [92]:
from src.data.procesamiento_datos import Preprocesamiento

pval = Preprocesamiento(['price'], ['price'])
set_validacion_transformado = set_validacion.pipe(preprocessing).pipe(pval.transform).pipe(build_features)[[variable_salida] + variables_entrada]
set_entrenamiento_transformado = set_entrenamiento.pipe(preprocessing).pipe(pval.transform).pipe(build_features)[[variable_salida] + variables_entrada]
In [93]:
modelo_optimizado.fit(set_entrenamiento_transformado[variables_entrada], set_entrenamiento_transformado[variable_salida])

y_real_train, y_predict_train = set_entrenamiento_transformado[variable_salida], modelo_optimizado.predict(set_entrenamiento_transformado[variables_entrada])
y_real_validation, y_predict_validation = set_validacion_transformado[variable_salida], modelo_optimizado.predict(set_validacion_transformado[variables_entrada])
In [94]:
r2_entrenamiento = r2_score(y_real_train, y_predict_train)
r2_validacion = r2_score(y_real_validation, y_predict_validation)

print(f'{r2_entrenamiento=:.2f}')
print(f'{r2_validacion=:.2f}')
r2_entrenamiento=0.73
r2_validacion=0.69

Se observa una disminución en el puntaje R2 de 4 puntos porcentuales, esto muestra que el modelo no tiene overfitting.

Implementacion del Modelo (Deploying)¶

Con el análisis básico y el ajuste hecho, comienza el trabajo real (ingeniería).

El último paso para poner en produccion el modelo de prediccion sera:

  1. Entrenarlo en todo el conjunto de datos nuevamente, para hacer un uso completo de todos los datos disponibles.
  2. Usar los mejores parámetros encontrados mediante la validación cruzada, por supuesto. Esto es muy similar a lo que hicimos al principio, pero esta vez teniendo una idea de su comportamiento y estabilidad. La evaluación se realizó con honestidad, en divisiones distintas de entrenamiento / prueba.

El predictor final se puede serializar y grabar en el disco, de modo que la próxima vez que lo usemos, podemos omitir todo el entrenamiento y usar el modelo capacitado directamente:

In [95]:
#import pickle # Esta es una libreria de serializacion nativa de python, puede tener problemas de seguridad

df_completo = pd.read_csv(du.raw_path.joinpath('kc_house_dataDS.csv'), index_col=0, decimal='.')
# df_transformado = df_completo.pipe(li.fit_transform).pipe(pre_pro.fit_transform).pipe(pval.fit_transform).pipe(build_features)
# df_transformado = df_transformado[[variable_salida] + variables_entrada]
_columnas_numericas = [columna for columna in df_completo.columns if columna != 'date']
pre_pro = Preprocesamiento(['price', 'sqft_lot', 'sqft_lot15'], [])
li = LimpiezaCalidad(_columnas_numericas)
pda = ProcesamientoDatos()

pipeline_tranformacion_prediccion = make_pipeline(
    li, pre_pro, pda
)

pipeline_tranformacion_entrenamiento=make_pipeline(
    li, pre_pro, pval, pda
)

df_transformado = pipeline_tranformacion_entrenamiento.fit_transform(df_completo)
pipeline_tranformacion_prediccion.fit(df_completo)
modelo_optimizado.fit(df_transformado[variables_entrada], df_transformado[variable_salida])

# Guardando
# garbar el modelo en un archivo
joblib.dump(pda, du.model_path.with_stem('pda'))

du.model = modelo_optimizado

Realizando la validación del modelo con los datos completos.

In [96]:
from sklearn.pipeline import Pipeline
df = pd.read_csv(du.raw_path.joinpath('kc_house_dataDS.csv'), index_col=0, decimal='.')

li = LimpiezaCalidad(_columnas_numericas)
pre_pro = Preprocesamiento(['price', 'sqft_lot', 'sqft_lot15'], [])
pda: ProcesamientoDatos = joblib.load(du.model_path.with_stem('pda'))

pipeline_tranformacion_prediccion = make_pipeline(
    li, pre_pro, pda
)

pipeline_tranformacion_validacion = make_pipeline(
    li, pre_pro, pval, pda
)
model: Pipeline = joblib.load(du.model_path)

df_transformado = pipeline_tranformacion_validacion.transform(df)
y_predict = model.predict(df_transformado)
y_real = df_transformado['price']
In [97]:
is_outlier = y_real.isna()

print(f'{r2_score(y_real[~is_outlier], y_predict[~is_outlier]):.2f}')
0.65
In [98]:
plots = [
    plot.scatter(pd.DataFrame({'y_real_train': y_real_train, 'y_predict_train':y_predict_train}), x='y_real_train', y='y_predict_train'),
    plot.scatter(pd.DataFrame({'y_real_train': y_real, 'y_predict_train':y_predict}), x='y_real_train', y='y_predict_train')
]
plots2 = [
    plot.histogram(pd.DataFrame({'y_real_train': y_real_train, 'y_predict_train':y_predict_train}), x='y_real_train'),
    plot.histogram(pd.DataFrame({'y_real': y_real, 'y_predict_train':y_predict}), x='y_real')
]

plot.grid_subplot(*plots, 
                  cols=2, 
                  titles=['División test-train', 'Dataset completo'],
                  title='Scater plot entre datos de entrenamiento y dataset completo',
                  height=500
                 ).show()

plot.grid_subplot(*plots2, 
                  cols=2, 
                  titles=['División test-train', 'Dataset completo'],
                  title='Distribución entre datos de entrenamiento y dataset completo',
                  height=500
                 ).show()
02M4M6M8M01M2M3M4M5M6M7M00.5M1M1.5M00.2M0.4M0.6M0.8M1M1.2M1.4M
Scater plot entre datos de entrenamiento y dataset completoDivisión test-trainDataset completo
plotly-logomark
22028511001832594105066657016547067537466486316437106355215074555095104363633643273432862712652472222311931741691611341151011099484803215441616351427111730133212132081785237161815310492061329112622545168110246216124392151474114121215121121421211432212111111112M4M6M01002003004005006007002111216233037418210211812919023628429533638043842244441342241242748445141050035742036941138337642040537141933231928932229426831430927633325726124521124819323019016524216718716015715815316213411615313013011511011910110792741157686617373547952377248564256301691210421291353210795271047102979810289510424394416353223317515210.5M1M0100200300400500
Distribución entre datos de entrenamiento y dataset completoDivisión test-trainDataset completo
plotly-logomark

Se observa una disminución en el R2 al entrenar con los datos completos, al revisar la distribución de los datos, se observa que en la eliminación de datos con el z-score, quedaron por fuera precios mayores de 2 millones, esto causa una disminución en el cálculo del R2. Esto será solucionado en la implementación del modelo en código.

Comunicacion de Resultados (Data Story Telling)¶

Gráfico de dependencia parcial.¶

Los gráficos de dependencia parcial son una forma de ver el impacto que tiene una variable en la respuesta. Se realizará el gráfico para las variables más importantes identificadas anteriormente: sqft_living, grade, sqft_above, sqft_living, bathrooms, view.

In [ ]:
import matplotlib.pyplot as plt
from sklearn.inspection import partial_dependence
# from sklearn.inspection import plot_partial_dependence
from sklearn.inspection import PartialDependenceDisplay
from time import time

features = ['sqft_living', 'sqft_lot', 'sqft_lot15', 'grade', 'view', 'bathrooms', 'bedrooms', 'sqft_living15', 
            'waterfront', 'floors', 'condition', 'fue_renovada', 'antiguedad_venta']

x = PartialDependenceDisplay.from_estimator(model, df_transformado.drop(columns='price'),features= features)

print('Computing partial dependence plots...')
In [100]:
fig, ax = plt.subplots(figsize=(12, 15))
x.plot(ax=ax)
fig
Out[100]:

Se puede observar que las siguientes variables no varían mucho el precio de venta de los hogares:

  • sqft_lot
  • floors
  • fue_renovada
  • bedrooms
  • bathrooms

Las siguientes tienen un efecto moderado en la salida:

  • sqft_living
  • sqft_living15
  • condition
  • view

Las siguientes variables tienen un fuerte efecto en el precio

  • waterfront
  • grade
  • antiguedad_venta

La siguiente variable tiene un leve impacto negativo en el precio

  • sqft_lot15

Conclusiones¶

  • Las variables que mayor impacto tienen en el precio de los hogares son variables relacionadas con el área habitable y entorno, como por ejemplo, el diseño, el tamaño, la vista, utilizacion de los interiores, vista a fuentes hidricas.
  • Se encontró que el mejor modelo es un modelo de regresión lineal con regularización (Ridge) con características polinomiales de segundo orden obteniendo un R2 de 65% con los datos completos, 73% con los datos de entrenamiento y 69% con los datos de validación, esta disminución corresponde a una eliminación excesiva de outliers, lo cual será revisado posteriormente para la mejora del modelo.
  • Una correcta limpieza de outliers fue fundamental para mejorar los resultados del modelo.

Recomendaciones Si actualmente se cuenta con una propiedad en Kansas y se tiene dinero disponible para invertir antes de realizar la venta, es recomendable antes de aumentar el tamaño del lote, invertir en la mejora del diseño de interior que ayude a mejorar los espacios y aumentar el área habitable pues se encontró que aunque la renovación aumenta el valor de las casas, si esta renovación no viene acompañada de un aumento en el valor estético de la misma no se logran obtener los mejores beneficios.

Ayudas Y Referencias¶

  • https://medium.com/@joserzapata/paso-a-paso-en-un-proyecto-machine-learning-bcdd0939d387

  • Proyecto de Principio a Final sobre readmision de pacientes con Diabetes

  • a-complete-machine-learning-walk-through-in-python-part-one

  • a-starter-pack-to-exploratory-data-analysis-with-python-pandas-seaborn-and-scikit-learn

  • a-data-science-for-good-machine-learning-project-walk-through-in-python-part-one

  • Ejemplos de Kaggle

  • END to END ML from data colletion to deployment

Docente: Jose R. Zapata

  • https://joserzapata.github.io
  • https://twitter.com/joserzapata
  • https://www.linkedin.com/in/jose-ricardo-zapata-gonzalez/